package com.example.demo17_simple_concurrentsession.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.session.HttpSessionEventPublisher;

/**
 * session 并发管理最大数为 1，并发策略为挤掉已经登录的用户，配置为：.sessionManagement().maximumSessions(1)
 * 流程分析：
 * 1. 当第一次登录成功后，会调用 ConcurrentSessionControlAuthenticationStrategy#onAuthentication() 方法，
 * 先从 SessionRegistry 取出没有过期的 session，判断是否超过最大 session 数，或是否是已经存在的 session，因为第一次登录，所以放行。
 * 在调用 RegisterSessionAuthenticationStrategy#onAuthentication() 方法，向 SessionRegistry 注册当前 session
 *
 * 2. 当第二次登录成功后，先调用 ConcurrentSessionControlAuthenticationStrategy#onAuthentication() 方法，
 * 先从 SessionRegistry 取出没有过期的 session，因为配置的最大 session 数为 1，之前已经登录过一次了，就会执行拦截方法 allowableSessionsExceeded(),
 * 因为并发管理配置的策略是 把之前已登录的挤下线，所以拦截方法的逻辑就会将 SessionRegistry 中多余的 session 设置为过期
 *
 * 3. 当尝试在第一次登录的客户端访问服务器时，会经过 ConcurrentSessionFilter 过滤器，从 SessionRegistry 取出当前 session，
 * 判断当前 session 是否过期，如果过期就执行 doLogout 和 SessionInformationExpiredStrategy#onExpiredSessionDetected 方法，
 * 清除 session 和 Authentication 认证信息，然后返回客户端 session 失效
 *
 *
 * session 并发管理最大数为 1，并发策略为禁止登录，配置为：.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)
 * 流程分析：
 * 1. 当第一次登录成功后，会调用 ConcurrentSessionControlAuthenticationStrategy#onAuthentication() 方法，
 * 先从 SessionRegistry 取出没有过期的 session，判断是否超过最大 session 数，或是否是已经存在的 session，因为第一次登录，所以放行。
 * 在调用 RegisterSessionAuthenticationStrategy#onAuthentication() 方法，向 SessionRegistry 注册当前 session
 *
 * 2. 当第二次登录成功后，先调用 ConcurrentSessionControlAuthenticationStrategy#onAuthentication() 方法，
 * 先从 SessionRegistry 取出没有过期的 session，因为配置的最大 session 数为 1，之前已经登录过一次了，就会执行拦截方法 allowableSessionsExceeded(),
 * 因为并发管理配置的策略是 禁止登录，所以拦截方法的逻辑为 抛出异常返回客户端 已经超过最大会话数量，禁止登录，
 *
 * 3. 这样配置的话存在一个小问题，是否超过最大会话数量是通过获取 SessionRegistry 中没有过期的 session 数量，
 * 但 SessionRegistry 中 session 记录是通过 监听 session 的销毁时间来 清除 SessionRegistry 中的记录，
 * 而当 注销登录时，session 被销毁，但 SessionRegistry 中的 session 还存在，就会导致再也登录不上去了
 * 所以还需要配置一个 HttpSessionEventPublisher 将 session 的创建和销毁发布出去，让 SessionRegistry 能够感知到，从而清除 SessionRegistry 中的 session 记录
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("wangnian").password("123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    //

    /**
     * 配置 HttpSessionEventPublisher，
     * 在该 Bean 中，可以将 session 创建以及销毁的事件及时感知到，
     * 并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去，进而被 Spring Security 感知到
     */
    @Bean
    HttpSessionEventPublisher httpSessionEventPublisher(){
        return new HttpSessionEventPublisher();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable()
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
        ;
    }
}
